using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Xml;
using ZoneFiveSoftware.Common.Data;
using ZoneFiveSoftware.Common.Data.Fitness;
using ZoneFiveSoftware.Common.Data.GPS;
using ZoneFiveSoftware.Common.Visuals;
using ZoneFiveSoftware.Common.Visuals.Fitness;

namespace LinuxGarminImport
{
    class ExtendImport : IExtendFitnessDevices
    {
        /// <summary>
        /// Logfile name containing recently imported files
        /// </summary>
        public static string Logname
        {
            get
            {
                // Logfile will be stored with the plugin DLL
                string logname = Assembly.GetExecutingAssembly().Location;
                logname = logname.Remove(logname.LastIndexOf(Path.DirectorySeparatorChar));
                logname += Path.DirectorySeparatorChar + "garminimport.log";

                return logname;
            }
        }

        #region IExtendFitnessDevices Members

        public IList<IFitnessDevice> FitnessDevices
        {
            get
            {
                IList<IFitnessDevice> devices = new List<IFitnessDevice>();
                devices.Add(new LinuxGarmin());
                return devices;
            }
        }

        #endregion
    }

    class LinuxGarmin : IFitnessDevice
    {
        #region IFitnessDevice Members

        /// <summary>
        /// Fitness Device Options button clicked
        /// </summary>
        /// <param name="configurationInfo"></param>
        /// <returns></returns>
        public string Configure(string configurationInfo)
        {
            DeviceOptions options = new DeviceOptions();

            // Set the icon in the form
            Bitmap bitmap = new Bitmap(CommonResources.Images.Import16);
            options.Icon = Icon.FromHandle(bitmap.GetHicon());
            options.ThemeChanged(PluginMain.GetApplication().VisualTheme);
            options.UICultureChanged(CultureInfo.CurrentCulture);

            // Display options form
            options.ShowDialog();
            options.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
            options.Dispose();

            return string.Empty;
        }

        /// <summary>
        /// Description displayed in Import selection list
        /// </summary>
        /// <param name="configurationInfo"></param>
        /// <returns></returns>
        public string ConfiguredDescription(string configurationInfo)
        {
            return Resources.StringResources.ConfiguredDescription;
        }

        #endregion

        #region IDataImporter Members

        public Guid Id
        {
            get { return GUIDs.FitnessDevice; }
        }

        /// <summary>
        /// Gets image displayed next to device in import dialog.
        /// </summary>
        public System.Drawing.Image Image
        {
            get
            {
                // Note that png displays better in Linux, and gif better in Windows.  Weird.
                if (Environment.OSVersion.Platform == PlatformID.Unix)
                {
                    return Resources.Images.ForerunnerTux48png;
                }
                else
                {
                    return Resources.Images.ForerunnerTux48gif;
                }
            }
        }

        /// <summary>
        /// Called by ST when import requested from Import Device.
        /// </summary>
        /// <param name="configurationInfo"></param>
        /// <param name="monitor">Progress indication</param>
        /// <param name="importResults">Store activity data here to be 'imported' into ST</param>
        /// <returns></returns>
        public bool Import(string configurationInfo, IJobMonitor monitor, IImportResults importResults)
        {
            ProcessStartInfo psi;
            string activityFilename;
            DateTime fileDate;
            Process procGarmin;
            float percentComp = 0;

            // Check that this is running on Linux
            // TODO: Check that Garmin-Forerunner-Tools is installed
            if (Environment.OSVersion.Platform == PlatformID.Unix)
            {
                monitor.StatusText = Resources.StringResources.ReadFromGarmin + "...";
                psi = new ProcessStartInfo("garmin_save_runs");
            }
            else
            {
                monitor.StatusText = Resources.StringResources.OnlySupportedInLinux;
                return false;
            }

            // Setup process to run and how to capture output
            // Command output is a list of files written and skipped, and this output is captured below
            psi.RedirectStandardOutput = true;
            psi.WindowStyle = ProcessWindowStyle.Hidden;
            psi.UseShellExecute = false;
            procGarmin = Process.Start(psi);
            StreamReader fileList = procGarmin.StandardOutput;

            string output = fileList.ReadToEnd();
            while (!procGarmin.HasExited)
            {
                procGarmin.WaitForExit(200);
                if (monitor.Cancelled)
                {
                    return false;
                }
            }
            // Data transfer from Garmin complete.
            monitor.PercentComplete = .5F;
            percentComp = .5F;

            // Create a new logfile each time
            StreamWriter logfile = new StreamWriter(ExtendImport.Logname, false);

            string[] result = output.Split(null);

            // TODO: 1) Do not include oddball track pauses (as detected by OMB MapOverlay)
            // TODO: 2) Add automatic 'rest' laps for interval workout activities --> Rest="true"
            foreach (string line in result)
            {
                try
                {
                    //Only import selected activities.  One pass through here = 1 imported activity
                    if (line.Contains(".gmn"))
                    {
                        // Write Logfile
                        logfile.WriteLine(line);

                        //Get filename (including path)
                        activityFilename = line.Substring(line.IndexOf(":") + 1).Trim();
                        int len = activityFilename.Length;
                        fileDate = new DateTime(int.Parse(activityFilename.Substring(len - 19, 4)), int.Parse(activityFilename.Substring(len - 15, 2)), int.Parse(activityFilename.Substring(len - 13, 2)), int.Parse(activityFilename.Substring(len - 10, 2)), int.Parse(activityFilename.Substring(len - 8, 2)), int.Parse(activityFilename.Substring(len - 6, 2)));
                        monitor.StatusText = CommonResources.Text.ActionImport + ": " + fileDate.ToString("g", CultureInfo.CurrentCulture);

                        // Import the binary GMN file
                        ActivityContainer importData = ImportGMN(activityFilename);

                        // Ensure this is a valid activity to import.  Much of what comes back are lap logs with no data tracks (skip these.)
                        if (importData.ValidImport)
                        {
                            // Import Activity
                            ProcessImport(importData, importResults);

                            // Display status
                            percentComp = Math.Min(((1F + percentComp) / 2F), percentComp + 0.1F);
                            monitor.PercentComplete = percentComp;
                        }
                    }
                    else if (line.Contains(Path.DirectorySeparatorChar.ToString()))
                    {
                        // Write Logfile
                        logfile.WriteLine(line);
                    }
                }
                catch (Exception e)
                {
                 	logfile.Write(e);
					break;
                }
            }

            logfile.Close();
            return true;
        }

        public string Name
        {
            get
            {
                return Resources.StringResources.ImportDeviceName;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Processes data retrieved from import, and creates activity out of it to be sent to ST via importResults
        /// </summary>
        /// <param name="importData">Data collected from import.</param>
        /// <param name="importResults">Mechanism for sending data (activity) back to ST</param>
        internal static void ProcessImport(ActivityContainer importData, IImportResults importResults)
        {
            if (importData.ValidImport)
            {
                string version = typeof(LinuxGarmin).Assembly.GetName().Version.ToString(2);

                IActivity activity = importResults.AddActivity(importData.StartTime);
                activity.GPSRoute = importData.GpsRoute;
                activity.DistanceMetersTrack = importData.DistanceData;
                activity.CadencePerMinuteTrack = importData.CadenceData;
                activity.HeartRatePerMinuteTrack = importData.HeartRateData;
                activity.Notes = Resources.StringResources.ActivityNote + version;
                activity.Metadata.Source = Resources.StringResources.ActivityNote + version;
                foreach (ValueRange<DateTime> pause in importData.TimerPauses)
                {
                    activity.TimerPauses.Add(pause);
                }


                float distanceMarker = 0;
                float totalCalories = 0;
                float totalDistance = 0;
                TimeSpan totalDuration = TimeSpan.Zero;

                foreach (ActivityContainer.Lap lap in importData.Laps)
                {
                    activity.Laps.Add(lap.StartTime, lap.Duration);
                    activity.Laps[activity.Laps.Count - 1].TotalCalories = lap.Calories;
                    activity.Laps[activity.Laps.Count - 1].Rest = (lap.Intensity == "rest");
                    totalCalories += lap.Calories;
                    totalDistance += lap.Distance;
                    totalDuration += lap.Duration;
                    distanceMarker += lap.Distance; //Distance Markers are cumulative
                    activity.DistanceMarkersMeters.Add(distanceMarker);
                }
                // Don't use the end point as a lap marker.
                activity.DistanceMarkersMeters.RemoveAt(activity.DistanceMarkersMeters.Count - 1);

                // Enter totaled data
                activity.TotalCalories = totalCalories;
                activity.TotalDistanceMetersEntered = totalDistance;
                activity.TotalTimeEntered = totalDuration;
            }
        }

        /// <summary>
        /// Reads Garmin dump file from 'garmin forerunner tools' utility.  This is used both by the file importer (output of garmin_dump) and the direct device download (which uses garmin_dump to create the file passed here.)
        /// </summary>
        /// <param name="gmnFilename">Dumped filename (including path)</param>
        /// <returns>Returns an activity wrapper containing all necessary imported activity data.</returns>
        internal static ActivityContainer ImportGMN(string gmnFilename)
        {
            ProcessStartInfo psi;
            Process procGarmin;
            ActivityContainer importData = new ActivityContainer();
            bool timerPause = false;
            DateTime pauseStart = DateTime.MinValue;

            StreamReader activityReader;

            // Convert output from binary file to XML-like stream
            if (Environment.OSVersion.Platform == PlatformID.Unix)
            {
                psi = new ProcessStartInfo("garmin_dump", gmnFilename);
                psi.RedirectStandardOutput = true;
                psi.WindowStyle = ProcessWindowStyle.Hidden;
                psi.UseShellExecute = false;
                procGarmin = Process.Start(psi);

                //Add <activity> root element to the activity (otherwise, no root element and error occurs)
                activityReader = procGarmin.StandardOutput;

            }
            else
            {
                // TODO: This is just a hack to let me import already converted files in Windows for debugging
                activityReader = new StreamReader(gmnFilename);
            }

            MemoryStream activityStream = new MemoryStream();
            StreamWriter sw = new StreamWriter(activityStream);

            sw.Write("<activity>");
            string gmnstuff = activityReader.ReadToEnd();
            sw.Write(gmnstuff);
            sw.Write("</activity>");
            sw.Flush();

            // Read raw garmin activity data
            activityStream.Seek(0, SeekOrigin.Begin);
            XmlReader xmlReader = new XmlTextReader(activityStream);

            // TODO: i is just for debugging.  Remove this later.
            int i = 0;
            while (xmlReader.Read())
            {
                i++;

                if (xmlReader.NodeType == XmlNodeType.Element)
                {
                    // Define variables used below.  Note that 'time' is used in both lap and track data
                    DateTime time = DateTime.MinValue;
                    TimeSpan duration = TimeSpan.Zero;
                    float lat = 0;
                    float lon = 0;
                    float alt = 0;
                    float distance = 0;
                    float calories;
                    float hr = 0;
                    float cadence = 0;
                    bool sensor = false;
                    string intensity = string.Empty;

                    switch (xmlReader.Name)
                    {
                        case "lap":
                            for (int j = 0; j < xmlReader.AttributeCount; j++)
                            {
                                xmlReader.MoveToAttribute(j);
                                switch (xmlReader.Name.ToLower())
                                {
                                    case "index":
                                        break;
                                    case "start":
                                        time = DateTime.Parse(xmlReader.Value);
                                        time = time.ToUniversalTime();
                                        break;
                                    case "duration":
                                        duration = TimeSpan.Parse(xmlReader.Value);
                                        break;
                                    case "distance":
                                        distance = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        break;
                                }
                            }

                            // Read calories (calories are recorded per lap in the Garmin)
                            calories = 0;
                            if (xmlReader.MoveToElement())
                            {
                                XmlReader lapReader = xmlReader.ReadSubtree();

                                lapReader.ReadToFollowing("calories");
                                lapReader.Read();
                                calories = float.Parse(lapReader.Value);

                                lapReader.ReadToFollowing("intensity");
                                lapReader.Read();
                                intensity = lapReader.Value;

                                lapReader.Close();
                            }

                            // Store Lap Data
                            importData.AddLap(time, duration, distance, calories, intensity);
                            break;

                        case "point":
                            for (int j = 0; j < xmlReader.AttributeCount; j++)
                            {
                                xmlReader.MoveToAttribute(j);
                                switch (xmlReader.Name.ToLower())
                                {
                                    case "time":
                                        time = DateTime.Parse(xmlReader.Value, System.Globalization.DateTimeFormatInfo.InvariantInfo);
                                        time = time.ToUniversalTime();
                                        break;

                                    case "lat":
                                        lat = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        break;

                                    case "lon":
                                        lon = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        break;

                                    case "alt":
                                        alt = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        break;

                                    case "distance":
                                        distance = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        importData.DistanceData.Add(time, distance);
                                        break;

                                    case "hr":
                                        hr = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        importData.HeartRateData.Add(time, hr);
                                        break;

                                    case "cadence":
                                        cadence = float.Parse(xmlReader.Value,CultureInfo.InvariantCulture.NumberFormat);
                                        importData.CadenceData.Add(time, cadence);
                                        break;
                                    case "sensor":
                                        bool.TryParse(xmlReader.Value, out sensor);
                                        break;
                                }
                            }

                            // Collect Track Pauses
                            if (lat != 0 && lon != 0)
                            {
                                importData.GpsRoute.Add(time, new GPSPoint(lat, lon, alt));
                                if (timerPause)
                                {
                                    // Found end of a pause.  Add pause to activity.
                                    importData.AddPause(pauseStart, time);
                                    timerPause = false;
                                }
                            }
                            else if (!timerPause && importData.StartTime != DateTime.MinValue && !sensor)
                            {
                                // Found beginning of a pause
                                timerPause = true;
                                pauseStart = time;
                            }

                            break;
                    }
                }
            }

            // Close streams after use
            sw.Close();
            activityStream.Close();
            activityReader.Close();

            return importData;
        }

        #endregion
    }

    class GMNFileImporter : IFileImporter
    {
        #region IFileImporter implementation

        /// <summary>
        /// File import routine.
        /// </summary>
        /// <param name="configurationInfo"></param>
        /// <param name="monitor"></param>
        /// <param name="importResults"></param>
        /// <returns></returns>
        public bool Import(string configurationInfo, IJobMonitor monitor, IImportResults importResults)
        {
            ActivityContainer importData = LinuxGarmin.ImportGMN(configurationInfo);
            LinuxGarmin.ProcessImport(importData, importResults);

            return true;
        }

        public Guid Id
        {
            get
            {
                return GUIDs.FileImport;
            }
        }

        public string Name
        {
            get
            {
                return Resources.StringResources.GMNImportName;
            }
        }

        public System.Drawing.Image Image
        {
            get
            {
                return CommonResources.Images.DocumentAdd16;
            }
        }

        #endregion

        #region IFileImporter Members

        public string FileExtension
        {
            get { return "GMN"; }
        }

        #endregion
    }

    class ExtendImportFile : IExtendDataImporters
    {
        #region IExtendDataImporters implementation

        public void BeforeImport(System.Collections.IList items)
        {
            //Nothing to do here.
        }

        public void AfterImport(System.Collections.IList added, System.Collections.IList updated)
        {
            //Replace LineFeed with CrLf in Notes section
            //IList<IActivity> activities = added as IList<IActivity>;
            //foreach (IActivity activity in activities)
            //{
            //    Console.WriteLine(activity.StartTime + ": " + activity.Notes.IndexOf("\n").ToString());
            //    activity.Notes = activity.Notes.Replace("\n", @" \r\n ");
            //}
        }

        public IList<IFileImporter> FileImporters
        {
            get
            {
                // TODO: Only do this for Linux machines?
                IList<IFileImporter> fileImporters = new List<IFileImporter>();
                fileImporters.Add(new GMNFileImporter());
                return fileImporters;
            }
        }

        #endregion
    }


}
